package pl.edu.agh.jemo.evolution.objfunc;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;

import pl.edu.agh.jemo.evolution.objfunc.impl.ExtremaFinderObjectiveFunction;
import pl.edu.agh.jemo.evolution.population.Population;
import pl.edu.agh.jemo.evolution.specimen.Specimen;

/**
 * Container for objective functions used together as multicriterial problem.
 * As an extension of java ArrayList it allows to perform every type of ArrayList actions to manipulates it's content.
 * It also introduces additional methods making useage of objective functions easier.
 * 
 * @author Marcin Zbijowski
 */
public class ObjectiveFunctionSet extends ArrayList<ObjectiveFunction> {

	/**
	 * Number of dimensions of specimen required for all contained functions to be able to calculate it's value.
	 */
	protected int dimensions = 0;
	/**
	 * Holds precalculated domain definition for each dimension based on all functions in container.
	 * Domain is assumed to be conntinous.
	 * First dimension of array refers to function dimension index.
	 * For each dimension two values are stored, lower (0) and upper (1) bound of domain, for specified dimension.
	 */
	protected Double[][] boundaries = null;
	// Dev: needed?

	/**
	 * Automatically generated ID for serialization purpose
	 */
	private static final long serialVersionUID = -1985747443563925855L;
	
	private static Comparator<ObjectiveFunction> functionComparator = new Comparator<ObjectiveFunction>() {

		@Override
		public int compare(ObjectiveFunction o1, ObjectiveFunction o2) {
			if(o1 instanceof ExtremaFinderObjectiveFunction){
				return 1;
			}
			if (o2 instanceof ExtremaFinderObjectiveFunction){
				return -1;
			}
			if ((o1 instanceof ClassicObjectiveFunction) && (o2 instanceof ClassicObjectiveFunction)) {
				return 0;
			}
			else if (o1 instanceof ClassicObjectiveFunction) {
				return -1;
			}
			return 1;
		}
	};
	
	public void sortSet() {
		Collections.sort(this, functionComparator);
	}

	/**
	 * Performs evaluation of given specimen from all criterias point of view.
	 * Calculations should be based on specimen Genotype, population reference
	 * and other parameters stored in specific ObjectiveFunction instances.
	 * Specimen reference should not change while calculations.
	 * It is advice in most cases to use returned value as specimen phenotype.
	 * 
	 * @param specimen Specimen reference for which values are calculated.
	 * @return Calculated array of Double values derived from specimen genotype, this value might be used as specimen phenotype.
	 */
	public Double[] calculateObjectiveFunctionsValues(Specimen specimen) {
		Double[] result = new Double[size()];
		for (int i = 0; i < size(); i++) {
			result[i] = get(i).calculateValue(specimen);
		}
		return result;
	}

	/**
	 * Changes population reference for all crowding objectice functions in set
	 * This method just iterates over all crowding objective functions stored in object.
	 * For more detailed description please refer to CrowdingObjectiveFunction class.
	 * 
	 * @param population New population reference to be used in crowding objective functions
	 */
	public void setPopulationReference(Population population) {
		for (ObjectiveFunction fun : this) {
			if (fun instanceof CrowdingObjectiveFunction) {
				((CrowdingObjectiveFunction) fun)
						.setPopulationReference(population);
			}
		}
	}

	/**
	 * Recalculates domain boundaries of objective functions set.
	 * This method is added for optimalization reason.
	 * It calculates intersection of domains for all classic objective functions in set,
	 * and uses it as a domain for generated specimen.
	 * Calculations are made only on set changes, assuming these take place only while initializing algorithm. 
	 */
	protected void calculateBoundaries() {
		if (dimensions == 0) {
			boundaries = null;
			return;
		}
		boundaries = new Double[dimensions][2];
		Double[] lower = new Double[dimensions];
		Double[] upper = new Double[dimensions];
		for (int i = 0; i < dimensions; i++) {
			lower[i] = -Double.MAX_VALUE;
			upper[i] = Double.MAX_VALUE;
		}
		for (ObjectiveFunction function : this) {
			if (function instanceof ClassicObjectiveFunction) {
				ClassicObjectiveFunction fun = (ClassicObjectiveFunction) function;
				for (int i = 0; i < fun.getDimensions(); i++) {
					if (lower[i] < fun.getBoundaries()[i][0]) {
						lower[i] = fun.getBoundaries()[i][0];
					}
					if (upper[i] > fun.getBoundaries()[i][1]) {
						upper[i] = fun.getBoundaries()[i][1];
					}
				}
			}
		}
		for (int i = 0; i < dimensions; i++) {
			boundaries[i][0] = lower[i];
			boundaries[i][1] = upper[i];
		}
	}

	/**
	 * Recalculates number of dimensions of objective functions set.
	 * This method is added for optimalization reason.
	 * It calculates maximum of dimensions for all classic objective functions in set,
	 * and uses it as a minimal number of dimensions for generated specimen.
	 * Calculations are made only on set changes, assuming these take place only while initializing algorithm. 
	 */
	protected void recalculateDimensions() {
		dimensions = 0;
		for (ObjectiveFunction function : this) {
			if (function instanceof ClassicObjectiveFunction) {
				ClassicObjectiveFunction fun = (ClassicObjectiveFunction) function;
				if (fun.getDimensions() > dimensions) {
					dimensions = fun.getDimensions();
				}
			}
		}
	}

	/**
	 * Tests if given specimen belongs to mutual function set domain.
	 * Test is made based on derived set domain and specimen genotype. 
	 * If specimen genotype is shorter than function dimensions, false is returned without further check.
	 * However specimen with genotypes longer than required are accepted. Surplus dimensions are skipped while testing.
	 * 
	 * @param specimen Queried specimen reference. It is assumed that specimen genotype array has been initialized.
	 * @return True if given specimen belongs to function set domain, flase otherwise 
	 */
	public boolean isInDomain(Specimen specimen) {
		if (dimensions == 0) {
			recalculateDimensions();
		}
		if (boundaries == null) {
			calculateBoundaries();
		}
		if (specimen.getGenotype().length < dimensions) {
			return false;
		}
		for (int i = 0; i < boundaries.length; i++) {
			if (!((specimen.getGenotype()[i].asDouble() >= boundaries[i][0]) && (specimen.getGenotype()[i].asDouble() < boundaries[i][1])))
				return false;
		}
		return true;
	}

	/**
	 * Returns two dimensional array of set domain boundaries.
	 * First dimension of array refers to function dimension index.
	 * For each dimension two values are stored, lower (0) and upper (1) bound of domain, for specified dimension.
	 *
	 * @return Two dimensional array of boundaries.
	 */
	public Double[][] getBoundaries() {
		if (boundaries == null) {
			calculateBoundaries();
		}
		return boundaries;
	}

	/**
	 * Returns dimensions of specimen genotype needed to compute values of all functions in set.
	 * 
	 * @return number of dimensions of function
	 */
	public int getDimensions() {
		return dimensions;
	}
	
	public Double[][] getMinMaxValues() {
		if (getDimensions() != 2) {
			throw new UnsupportedOperationException("applicable only to two-dimensional problems");
		}
		final Double[][] minMaxValues = new Double[2][2];
		minMaxValues[0][0] = this.get(0).getMinValue();
		minMaxValues[0][1] = this.get(0).getMaxValue();
		minMaxValues[1][0] = this.get(1).getMinValue();
		minMaxValues[1][1] = this.get(1).getMaxValue();
		return minMaxValues;
	}

	/**
	 * Appends the specified objective function to the end of this list.
	 * Forces domain boundaries and dimensions recalculation
	 * 
	 * @param function objective function to be appended to this list.
	 * @return true (as per the general contract of Collection.add).
	 */
	@Override
	public boolean add(ObjectiveFunction function) {
		boolean result = super.add(function);
		if (function instanceof ClassicObjectiveFunction) {
			ClassicObjectiveFunction fun = (ClassicObjectiveFunction) function;
			boundaries = null;
			if (dimensions < fun.getDimensions()) {
				dimensions = fun.getDimensions();
			}
		}
		return result;
	}

	/**
	 * Inserts the specified element at the specified position in this list.
	 * Shifts the element currently at that position (if any) and any subsequent elements to the right (adds one to their indices).
	 * 
	 * @param index index at which the specified element is to be inserted.
	 * @param function objective function to be appended to this list.
	 * @throws IndexOutOfBoundsException if index is out of range (index < 0 || index > size()).
	 */
	@Override
	public void add(int index, ObjectiveFunction function) {
		super.add(index, function);
		if (function instanceof ClassicObjectiveFunction) {
			ClassicObjectiveFunction fun = (ClassicObjectiveFunction) function;
			boundaries = null;
			if (dimensions < fun.getDimensions()) {
				dimensions = fun.getDimensions();
			}
		}
	}
	/**
	 * Removes a single instance of the specified element from this collection, if it is present (optional operation).
	 * More formally, removes an element e such that (o==null ? e==null : o.equals(e)), if the collection contains one or more such elements. Returns true if the collection contained the specified element (or equivalently, if the collection changed as a result of the call).
	 * This implementation iterates over the collection looking for the specified element. If it finds the element, it removes the element from the collection using the iterator's remove method.
	 * Note that this implementation throws an UnsupportedOperationException if the iterator returned by this collection's iterator method does not implement the remove method and this collection contains the specified object.
	 * 
	 * @param o element to be removed from this collection, if present.
	 * @return true if the collection contained the specified element.
	 * @throws UnsupportedOperationException if the remove method is not supported by this collection.
	 */
	@Override
	public boolean remove(Object o) {
		boundaries = null;
		boolean result = false;
		if (o instanceof ObjectiveFunction) {
			result = super.remove(o);
			if ((o instanceof ClassicObjectiveFunction)
					&& ((ClassicObjectiveFunction) o).getDimensions() == dimensions) {
				recalculateDimensions();
			}
		}
		return result;
	}

	/**
	 * Appends all of the elements in the specified Collection to the end of this list, in the order that they are returned by the specified Collection's Iterator.
	 * The behavior of this operation is undefined if the specified Collection is modified while the operation is in progress. (This implies that the behavior of this call is undefined if the specified Collection is this list, and this list is nonempty.)
	 * 
	 * @param c the elements to be inserted into this list.
	 * @return true if this list changed as a result of the call.
	 * @throws NullPointerException if the specified collection is null.
	 */
	@Override
	public boolean addAll(Collection<? extends ObjectiveFunction> c) {
		boundaries = null;
		boolean result = super.addAll(c);
		recalculateDimensions();
		return result;
	}

	/**
	 * Inserts all of the elements in the specified Collection into this list, starting at the specified position.
	 * Shifts the element currently at that position (if any) and any subsequent elements to the right (increases their indices). The new elements will appear in the list in the order that they are returned by the specified Collection's iterator.
	 * 
	 * @param index index at which to insert first element from the specified collection.
	 * @param c the elements to be inserted into this list.
	 * @return true if this list changed as a result of the call.
	 * @throws IndexOutOfBoundsException if index out of range (index < 0 || index > size()).
	 * @throws NullPointerException if the specified Collection is null.
	 */
	@Override
	public boolean addAll(int index, Collection<? extends ObjectiveFunction> c) {
		boundaries = null;
		boolean result = super.addAll(index, c);
		recalculateDimensions();
		return result;
	}

	/**
	 * Removes from this collection all of its elements that are contained in the specified collection (optional operation).
	 * This implementation iterates over this collection, checking each element returned by the iterator in turn to see if it's contained in the specified collection. If it's so contained, it's removed from this collection with the iterator's remove method.
	 * Note that this implementation will throw an UnsupportedOperationException if the iterator returned by the iterator method does not implement the remove method and this collection contains one or more elements in common with the specified collection.
	 * 
	 * @param c elements to be removed from this collection.
	 * @return true if this collection changed as a result of the call.
	 * @throws UnsupportedOperationException if the removeAll method is not supported by this collection.
	 * @throws NullPointerException if the specified collection is null.
	 */
	@Override
	public boolean removeAll(Collection<?> c) {
		boundaries = null;
		boolean result = super.removeAll(c);
		recalculateDimensions();
		return result;
	}

	/**
	 * Retains only the elements in this collection that are contained in the specified collection (optional operation). In other words, removes from this collection all of its elements that are not contained in the specified collection.
	 * This implementation iterates over this collection, checking each element returned by the iterator in turn to see if it's contained in the specified collection. If it's not so contained, it's removed from this collection with the iterator's remove method.
	 * Note that this implementation will throw an UnsupportedOperationException if the iterator returned by the iterator method does not implement the remove method and this collection contains one or more elements not present in the specified collection.
	 * 
	 * @param c elements to be retained from this collection.
	 * @return true if this collection changed as a result of the call.
	 * @throws UnsupportedOperationException if the retainAll method is not supported by this collection.
	 * @throws NullPointerException if the specified collection is null.
	 */
	@Override
	public boolean retainAll(Collection<?> c) {
		boundaries = null;
		boolean result = super.retainAll(c);
		recalculateDimensions();
		return result;
	}

	/**
	 * Removes all of the elements from this list. The list will be empty after this call returns.
	 */
	@Override
	public void clear() {
		super.clear();
		boundaries = null;
		dimensions = 0;
	}

	/**
	 * Replaces the element at the specified position in this list with the specified element.
	 * 
	 * @param index index of element to replace.
	 * @param element element to be stored at the specified position.
	 * @return the element previously at the specified position.
	 * @throws IndexOutOfBoundsException if index out of range (index < 0 || index >= size()).
	 */
	@Override
	public ObjectiveFunction set(int index, ObjectiveFunction element) {
		boundaries = null;
		ObjectiveFunction result = super.set(index, element);
		recalculateDimensions();
		return result;
	}

	/**
	 * Removes the element at the specified position in this list.
	 * Shifts any subsequent elements to the left (subtracts one from their indices).
	 * 
	 * 
	 */
	@Override
	public ObjectiveFunction remove(int index) {
		ObjectiveFunction result = get(index);
		remove(result);
		return result;
	}
	
	/**
	 * Returns contained objective functions in string format.
	 * Iterates over all set and adds ObjectiveFunction's toString printout. Prints both Crowding and Classic functions in the same way.
	 * 
	 *  @return String containing list of all functions.
	 */
	@Override
	public String toString() {
		StringBuilder sb = new StringBuilder();
		sb.append("Objective functions:\n");
		for (ObjectiveFunction fun : this) {
			sb.append(fun.toString() + "\n");
		}
		return sb.toString();
	}

	@Override
	public int hashCode() {
		return 1;
	}

	@Override
	public boolean equals(Object obj) {
		if (this == obj)
			return true;
		if (!super.equals(obj))
			return false;
		if (getClass() != obj.getClass())
			return false;
		ObjectiveFunctionSet other = (ObjectiveFunctionSet) obj;
		if (!Arrays.equals(boundaries, other.boundaries))
			return false;
		if (dimensions != other.dimensions)
			return false;
		return true;
	}
}
